package com.chimm.service.impl;

import com.chimm.dao.SeckillDao;
import com.chimm.dao.SuccessKilledDao;
import com.chimm.dao.cache.RedisDao;
import com.chimm.dto.Exposer;
import com.chimm.dto.SeckillExecution;
import com.chimm.entity.Seckill;
import com.chimm.entity.SuccessKilled;
import com.chimm.enums.SeckillStateEnum;
import com.chimm.excetion.RepeatKillException;
import com.chimm.excetion.SeckillCloseException;
import com.chimm.excetion.SeckillException;
import com.chimm.service.SeckillService;
import org.apache.commons.collections.MapUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.DigestUtils;

import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 秒杀服务实现类
 */
//@Component  @Service @Dao @Controller
@Service
public class SeckillServiceImpl implements SeckillService {

    // 定义日志对象
    private Logger logger = LoggerFactory.getLogger(this.getClass());


    // 注入dao
    @Autowired
    private SeckillDao seckillDao;
    @Autowired
    private SuccessKilledDao successKilledDao;
    @Autowired
    private RedisDao redisDao;

    // md5盐值字符串,用于混淆md5
    private final String slat = "spoq2cm&^4";

    /**
     * 查询所有秒杀记录
     *
     * @return List<Seckill>
     */
    public List<Seckill> getSeckkillList() {
        return seckillDao.queryAll(0, 4);
    }

    /**
     * 查询单个秒杀结果
     *
     * @param seckillId 秒杀商品ID
     * @return
     */
    public Seckill getById(long seckillId) {
        return seckillDao.queryById(seckillId);
    }

    /**
     * 秒杀开启时输出秒杀接口地址,
     * 否则输出系统时间和秒杀时间
     *
     * @param seckillId 秒杀商品ID
     * @return Exposer 暴露秒杀地址DTO
     */
    public Exposer exportSeckillUrl(long seckillId) {
        // 优化点：缓存优化，一致性维护，超时的基础上维护一致性
        /*----------------缓存优化------------------*/

        // 1_优化：访问redis
        Seckill seckill = redisDao.getSeckill(seckillId);
        if (seckill == null) {
            //没有拿到对象

            //2_优化：访问数据库

            // 1.调用秒杀dao接口，查询秒杀商品
            seckill = seckillDao.queryById(seckillId);
            // 2.测试用例：判断是否查询到了该商品
            if (seckill == null) {
                // 没有查询到秒杀商品，则返回该商品没有开启秒杀
                return new Exposer(false, seckillId);
            } else {
                //3_优化：存入redis
                redisDao.putSeckill(seckill);
            }
        }

        /*-----------------------------------------*/



        // 3.查询到了秒杀商品之后判断是否在秒杀时间段内
        long startTime = seckill.getStartTime().getTime();
        long endTime = seckill.getEndTime().getTime();

        // 3.1 获取当前系统时间
        long now = new Date().getTime();

        // 判断是否在秒杀时间内
        if (now < startTime || now > endTime) {
            // 不再范围内，返回该商品没有开启秒杀
            return new Exposer(false, seckillId);
        }

        // else 在时间范围内，补全exposer对象属性
        // 获取md5加密信息
        String md5 = getMD5(seckillId);

        return new Exposer(true, md5, seckillId);
    }

    /**
     * 获取md5加密后的字符串
     * @param seckillId 秒杀商品ID
     * @return md5
     */
    private String getMD5(long seckillId) {
        String base = seckillId + "/" + slat;
        String md5 = DigestUtils.md5DigestAsHex(base.getBytes());
        return md5;
    }

    /**
     * 执行秒杀
     *
     * @param seckillId 秒杀商品ID
     * @param userPhone 用户手机号
     * @param md5
     */
    @Transactional
    /**
     * 使用注解控制事务方法的优点：
     *  1：开发团队达成一致约定，明确标注事务方法的编程风格。
     *  2：保证事务方法的执行时间尽可能短，不要穿插其他网络的操作（redis、RPC/HTTP请求等，如果需要这些操作，将这些操作剥离到事务方法外部）
     *  3：不是所有的方法都需要事务，如只有一条修改操作，只读操作不需要事务控制。
     */
    public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5) throws SeckillException, RepeatKillException, SeckillCloseException {
        try {
            // 1.判断用户传递过来的seckillId 和 md5 是否匹配
            if (md5 == null || !md5.equals(getMD5(seckillId))) {
                // seckillId和md5没能匹配上，证明md5被重写了
                throw new SeckillException("seckill data rewrite");
            }


            /*----------修改逻辑，高并发优化------------*/
            // 2.记录购买行为
            int insertCount = successKilledDao.insertSuccessKilled(seckillId, userPhone);
            // 唯一约束：seckillId userPhone
            if (insertCount <= 0) {
                // 重复秒杀 rollback
                throw new RepeatKillException("seckill repeated");
            } else {
                // 购买行为记录成功，不是重复秒杀

                /*=========热点商品竞争、高并发在此处产生============*/
                // 3. 减库存
                int effectRows = seckillDao.reduceNumber(seckillId, new Date());
                /*=========热点商品竞争、高并发在此处产生============*/

                // 4.判断有没有成功秒杀
                if (effectRows <= 0) {
                    // 没有更新到记录，秒杀结束 rollback
                    throw new SeckillCloseException("seckill is closed");
                } else {
                    // 成功秒杀，查询该用户的秒杀商品详情，并返回 commit
                    SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(seckillId, userPhone);
                    return new SeckillExecution(seckillId, SeckillStateEnum.SUCCESS, successKilled);
                }
            }

            /*-----------------------------------------*/

//            // 2.通过判断后，执行秒杀逻辑，1.减库存、 2.记录购买行为
//            int effectRows = seckillDao.reduceNumber(seckillId, new Date());
//
//            // 3.判断有没有成功秒杀
//            if (effectRows <= 0) {
//                // 没有更新到记录，秒杀结束
//                throw new SeckillCloseException("seckill is closed");
//            } else {
//                // 成功秒杀，记录购买行为
//                int insertCount = successKilledDao.insertSuccessKilled(seckillId, userPhone);
//                // 唯一约束：seckillId userPhone
//                if (insertCount <= 0) {
//                    // 重复秒杀
//                    throw new RepeatKillException("seckill repeated");
//                } else {
//                    // 秒杀成功，查询秒杀记录，并返回
//                    SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(seckillId, userPhone);
//                    return new SeckillExecution(seckillId, SeckillStateEnum.SUCCESS, successKilled);
//                }
//            }
        } catch (SeckillCloseException e){
            throw e;
        } catch (RepeatKillException e){
            throw e;
        } catch (SeckillException e) {
            logger.error(e.getMessage(), e);
            // 所有编译器异常，转换为运行时异常
            throw new SeckillException("seckill inner error:"+e.getMessage());
        }
    }

    /**
     * 执行秒杀 by 存储过程
     *
     * @param seckillId 秒杀商品ID
     * @param userPhone 用户手机号
     * @param md5
     */
    public SeckillExecution executeSzeckillByProcedure(long seckillId, long userPhone, String md5) {
        // 1.先判断md5是否被篡改
        if (md5 == null || !md5.equals(getMD5(seckillId))) {
            return new SeckillExecution(seckillId, SeckillStateEnum.DATA_REWRITE);
        }
        Date killTime = new Date();
        Map<String, Object> map = new HashMap<>();
        map.put("seckillId", seckillId);
        map.put("phone", userPhone);
        map.put("killTime", killTime);
        map.put("result", null);

        //执行存储过程，result被赋值
        try {
            seckillDao.killByProcedure(map);
            // 获取result
            int result = MapUtils.getInteger(map, "result", -2);
            if (result == 1) {
                //秒杀成功
                SuccessKilled sk = successKilledDao.queryByIdWithSeckill(seckillId, userPhone);
                return new SeckillExecution(seckillId, SeckillStateEnum.SUCCESS, sk);
            } else {
                return new SeckillExecution(seckillId, SeckillStateEnum.stateOf(result));
            }
        } catch (Exception e) {
            logger.error(e.getMessage(),e);
            return new SeckillExecution(seckillId, SeckillStateEnum.INNER_ERROR);
        }
    }
}
